﻿using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace LocalizationUtils.Utils
{
    public abstract class HyperLinkTextBase : Text, IPointerClickHandler
    {
        private const int charVerts = 6;
        private const char Tab = '\t', LineFeed = '\n', Space = ' ', LesserThan = '<', GreaterThan = '>';
        private char[] invisibleChars = { Space, Tab, LineFeed };
        private int[] visibleCharIndexMap;
        private UIVertex[] tempVerts = new UIVertex[4];

        private global::LocalizationUtils.GMPool<List<UIVertex>> verticesPool =
            new global::LocalizationUtils.GMPool<List<UIVertex>>(null, l => l.Clear());

        private List<LinkInfo> linkInfos = new List<LinkInfo>();
        private Canvas rootCanvas;
        private Canvas RootCanvas => rootCanvas ? rootCanvas : (rootCanvas = GetComponentInParent<Canvas>());

        protected void AddLinkInfo(int startIndex, int length, Color color, Action<string> onClick)
        {
            linkInfos.Add(new LinkInfo(startIndex, length, color, onClick));
        }

        protected override void OnPopulateMesh(VertexHelper toFill)
        {
            if (font == null)
            {
                return;
            }

            m_DisableFontTextureRebuiltCallback = true;
            var extents = rectTransform.rect.size;
            var settings = GetGenerationSettings(extents);
            settings.generateOutOfBounds = true;
            cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);
            var verts = cachedTextGenerator.verts;
            var unitsPerPixel = 1 / pixelsPerUnit;
            int verCount = verts.Count;
            if (verCount <= 0)
            {
                toFill.Clear();
                return;
            }

            var roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
            roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
            toFill.Clear();
            if (roundingOffset != Vector2.zero)
            {
                for (int i = 0; i < verCount; ++i)
                {
                    int tempVertsIndex = i & 3;
                    tempVerts[tempVertsIndex] = verts[i];
                    tempVerts[tempVertsIndex].position *= unitsPerPixel;
                    tempVerts[tempVertsIndex].position.x += roundingOffset.x;
                    tempVerts[tempVertsIndex].position.y += roundingOffset.y;
                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(tempVerts);
                    }
                }
            }
            else
            {
                for (int i = 0; i < verCount; ++i)
                {
                    int tempVertsIndex = i & 3;
                    tempVerts[tempVertsIndex] = verts[i];
                    tempVerts[tempVertsIndex].position *= unitsPerPixel;
                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(tempVerts);
                    }
                }
            }

            var vertices = verticesPool.Get();
            toFill.GetUIVertexStream(vertices);
            GenerateVisibleCharIndexMap(vertices.Count < text.Length * charVerts);
            linkInfos.Clear();
            AddListeners();
            GenerateHrefBoundingBoxes(ref vertices);
            toFill.Clear();
            toFill.AddUIVertexTriangleStream(vertices);
            verticesPool.Release(vertices);
            m_DisableFontTextureRebuiltCallback = false;
        }

        void GenerateVisibleCharIndexMap(bool verticesReduced)
        {
            if (visibleCharIndexMap == null || visibleCharIndexMap.Length < text.Length)
            {
                Array.Resize(ref visibleCharIndexMap, text.Length);
            }

            if (!verticesReduced)
            {
                for (int i = 0; i < visibleCharIndexMap.Length; ++i)
                {
                    visibleCharIndexMap[i] = i;
                }

                return;
            }

            var offset = 0;
            var inTag = false;
            for (int i = 0; i < text.Length; ++i)
            {
                var character = text[i];
                if (inTag)
                {
                    offset--;
                    if (character == GreaterThan)
                    {
                        inTag = false;
                    }
                }
                else if (supportRichText && character == LesserThan)
                {
                    offset--;
                    inTag = true;
                }
                else if (invisibleChars.Contains(character))
                {
                    offset--;
                }

                visibleCharIndexMap[i] = Mathf.Max(0, i + offset);
            }
        }

        void GenerateHrefBoundingBoxes(ref List<UIVertex> vertices)
        {
            int verticesCount = vertices.Count;
            for (int i = 0; i < linkInfos.Count; ++i)
            {
                var linkInfo = linkInfos[i];
                var startIndex = visibleCharIndexMap[linkInfo.StartIndex];
                var endIndex = visibleCharIndexMap[linkInfo.StartIndex + linkInfo.Length - 1];
                for (int textIndex = startIndex; textIndex <= endIndex; ++textIndex)
                {
                    var vertexStartIndex = textIndex * charVerts;
                    if (vertexStartIndex + charVerts > verticesCount)
                    {
                        break;
                    }

                    var min = Vector2.one * float.MaxValue;
                    var max = Vector2.one * float.MinValue;

                    for (var vertexIndex = 0; vertexIndex < charVerts; ++vertexIndex)
                    {
                        var vertex = vertices[vertexStartIndex + vertexIndex];
                        vertex.color = linkInfo.Color;
                        vertices[vertexStartIndex + vertexIndex] = vertex;
                        var pos = vertices[vertexStartIndex + vertexIndex].position;
                        if (pos.y < min.y)
                        {
                            min.y = pos.y;
                        }

                        if (pos.x < min.x)
                        {
                            min.x = pos.x;
                        }

                        if (pos.y > max.y)
                        {
                            max.y = pos.y;
                        }

                        if (pos.x > max.x)
                        {
                            max.x = pos.x;
                        }
                    }

                    linkInfo.BoundingBoxes.Add(new Rect { min = min, max = max });
                }

                linkInfo.BoundingBoxes = CalculateLineBoundingBoxes(linkInfo.BoundingBoxes);
            }
        }

        private static List<Rect> CalculateLineBoundingBoxes(List<Rect> charBoundingBoxes)
        {
            var lineBoundingBoxes = new List<Rect>();
            var lineStartIndex = 0;
            for (var i = 1; i < charBoundingBoxes.Count; ++i)
            {
                if (charBoundingBoxes[i].xMin >= charBoundingBoxes[i - 1].xMin)
                {
                    continue;
                }

                lineBoundingBoxes.Add(CalculateAABB(charBoundingBoxes.GetRange(lineStartIndex, i - lineStartIndex)));
                lineStartIndex = i;
            }

            if (lineStartIndex < charBoundingBoxes.Count)
            {
                lineBoundingBoxes.Add(
                    CalculateAABB(charBoundingBoxes.GetRange(lineStartIndex,
                        charBoundingBoxes.Count - lineStartIndex)));
            }

            return lineBoundingBoxes;
        }

        private static Rect CalculateAABB(IReadOnlyList<Rect> rects)
        {
            var min = Vector2.one * float.MaxValue;
            var max = Vector2.one * float.MinValue;
            for (int i = 0; i < rects.Count; ++i)
            {
                if (rects[i].xMin < min.x)
                {
                    min.x = rects[i].xMin;
                }

                if (rects[i].yMin < min.y)
                {
                    min.y = rects[i].yMin;
                }

                if (rects[i].xMax > max.x)
                {
                    max.x = rects[i].xMax;
                }

                if (rects[i].yMax > max.y)
                {
                    max.y = rects[i].yMax;
                }
            }

            return new Rect { min = min, max = max };
        }

        protected abstract void AddListeners();

        public virtual void RemoveListeners()
        {
            linkInfos.Clear();
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            var localPosition = CalculateLocalPosition(eventData.position, eventData.pressEventCamera);
            for (int i = 0; i < linkInfos.Count; ++i)
            {
                for (int boxIndex = 0; boxIndex < linkInfos[i].BoundingBoxes.Count; ++boxIndex)
                {
                    if (linkInfos[i].BoundingBoxes[boxIndex].Contains(localPosition))
                    {
                        linkInfos[i].Callback(text.Substring(linkInfos[i].StartIndex, linkInfos[i].Length));
                        break;
                    }
                }
            }
        }

        private Vector3 CalculateLocalPosition(Vector3 position, Camera camera)
        {
            if (!RootCanvas)
            {
                return Vector3.zero;
            }

            if (RootCanvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                return transform.InverseTransformPoint(position);
            }

            RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, position, camera,
                out var localPosition);
            return localPosition;
        }

        private class LinkInfo
        {
            public int StartIndex;
            public int Length;
            public Color Color;
            public Action<string> Callback;
            public List<Rect> BoundingBoxes;

            public LinkInfo(int startIndex, int length, Color color, Action<string> callback)
            {
                this.StartIndex = startIndex;
                this.Length = length;
                this.Color = color;
                this.Callback = callback;
                this.BoundingBoxes = new List<Rect>();
            }
        }
    }
}